package de.ba_rm.java.password.checker.logic;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringUtils;

import de.ba_rm.java.password.checker.Messages;
import de.ba_rm.java.password.checker.logic.config.Rule;

/**
 * PasswordChecker ueberprueft, dass das eingegebene Passwort mit folgenden Kriterien uebereinstimmt:
 * <ul>
 * 	<li>es besteht aus acht Zeichen,</li>
 *  <li>zwei Zeichen sind Buchstaben,</li>
 *  <li>zwei Zeichen sind Ziffern,</li>
 *  <li>ein Gross- und ein Kleinbuchstabe sind enthalten,</li>
 *  <li>ein Sonderzeichen ist enthalten,</li>
 *  <li>mindestens eine Ziffer oder ein Sonderzeichen befindet sich innerhalb des Passwortes.</li>
 * </ul>
 * 
 * @author kluge.richard@googlemail.com
 * @author lehmann1904@gmail.com
 * @author s.stafflinger@gmail.com
 */
public class PasswordChecker {
	
	private String name;
	
	private int minLength;
	
	private int numLowerCase;
	
	private int numUpperCase;
	
	private int numCase;
	
	private int numSpecial;
	
	private int numNumbers;
	
	private int numSpecialBetweenChar;
	
	/**
	 * default c-tor
	 */
	public PasswordChecker() {
		this(Rule.NAME_DEFAULT, 8, 1, 1, 2, 1, 2, 1);
	}

	/**
	 * 
	 * @param minLength - Minimale Laenge des Passwortes
	 * @param numLowerCase - Anzahl der Kleinbuchstaben
	 * @param numUpperCase - Anzahl der Grossbuchstaben
	 * @param numCase - Anzahl der Buchstaben
	 * @param numSpecial - Anzahl der Sonderzeichen
	 * @param numNumbers - Anzahl der Ziffern
	 * @param numSpecialBetweenChar - Anzahl der Nummern/Sonderzeichen zwischen Buchstaben
	 */
	private PasswordChecker (String name, int minLength, int numLowerCase, int numUpperCase, int numCase, int numSpecial, int numNumbers, int numSpecialBetweenChar){
		this.name = name;
		this.minLength = minLength;
		this.numLowerCase = numLowerCase;
		this.numUpperCase = numUpperCase;
		this.numCase = numCase;
		this.numSpecial = numSpecial;
		this.numNumbers = numNumbers;
		this.numSpecialBetweenChar = numSpecialBetweenChar;
	}
	
	/**
	 * 
	 * @param rule
	 */
	public PasswordChecker(Rule rule) {
		this(rule.getName(), rule.getMinLength(), rule.getNumLowerCase(), rule.getNumUpperCase(), rule.getNumCase(), rule.getNumSpecial(), rule.getNumNumbers(), rule.getNumSpecialBetweenChar());
	}

	/**
	 * 
	 * @param password
	 * @return
	 */
	public List<String> checkPassword(final String password) {
		
		if (StringUtils.isBlank(password)) {
			throw new NullPointerException(Messages.getString("Exception.1"));
		}
		
		/*
		 * @variable isCorrect Gibt an, ob die Kriterien erfuellt worden sind
		 * @variable specialBetweenChart Counter fuer Sonderzeichen bzw. Zahlen, die sich innerhalb des Passwortes befinden
		 * @variable cntUpperCase Counter fuer Grossbuchstaben
		 * @variable cntLowerCase Counter fuer Kleinbuchstaben
		 * @variable cntNumber Counter fuer Ziffern
		 * @variable cntSpecial Counter fuer Sonderzeichen
		 * @variable firstChar Stelle des ersten Buchstaben. Dient als Hilfsparameter zur Ueberpruefung, ob sich ein Sonderzeichen bzw. eine Zahl innerhalb des Passwortes befindet
		 * @variable lastchar Stelle des letzen Buchstaben. Dient als Hilfsparameter zur Ueberpruefung ob sich ein Sonderzeichen bzw. eine Zahl innerhalb des Passwortes befindet
		 */
		final List<String> returnMessage = new ArrayList<String>();
		int specialBetweenChar = 0;
		int cntUpperCase = 0;
		int cntLowerCase = 0;
		int cntNumber = 0;
		int cntSpecial = 0;
		int firstChar = -1;
		int lastChar = -1;
		
		/*
		 * Schleife zur Typisierung der einzelnen Zeichen
		 * @variable i Zeichen an der Stelle i im Passwort. Wird nach der Typisierung erhoeht, um naechstes Zeichen zu typisieren.
		 * Ist ein Zeichen typisiert worden erhoeht sich der Counter des jeweiligen Typs.
		 */
		for(int i=0; i<password.length(); i++){
			char c = password.charAt(i); //Auswahl des Zeichens
			
			/*
			 * Falls es sich um einen Grossbuchstaben handelt,
			 * wird der Counter fuer Grossbuchstaben erhoeht.
			 * Der Parameter lastChar wird auf i gesetzt
			 * und, sofern der Parameter firstChar noch wie anfangs auf -1 gesetzt ist,
			 * wird der Parameter firstChar auf i gesetzt.
			 */
			if (Character.isUpperCase(c)) {
				cntUpperCase++;
				lastChar = i;
				if (firstChar == -1) {
					firstChar = i;
				}
			} 
			
			/*
			 * Falls es sich um einen Kleinbuchstaben handelt,
			 * wird der Counter fuer Kleinbuchstaben erhoeht.
			 * Der Parameter lastChar wird auf i gesetzt
			 * und, sofern der Parameter firstChar noch wie anfangs auf -1 gesetzt ist,
			 * wird der Parameter firstChar auf i gesetzt.
			 */
			else if (Character.isLowerCase(c)) {
				cntLowerCase++;
				lastChar = i;
				if (firstChar == -1) {
					firstChar = i;
				}
			} 
		 	//Falls es sich um eine Ziffer handelt, wird der Counter fuer Ziffern erhoeht. 
			else if (Character.isDigit(c)) {
				cntNumber++;
			} 	
			// Falls es sich um ein Sonderzeichen handelt, wird der Counter fuer Sonderzeichen erhoeht. 
			else {
				cntSpecial++;
			}
		}
		
		/*
		 * Schleife zur Ueberpuefung, ob sich ein Sonderzeichen bzw. eine Zahl innerhalb des Passwortes befindet.
		 * @param i Stelle des ersten Buchstaben im Passwort. Wird nach der Ueberpruefung erhoeht.
		 * Durch i<lastChar wird sichergestellt, dass nur die Zeichen zwischen dem ersten und dem letzten Buchstaben ueberprueft werden. 
		 */
		for (int i=firstChar; i<lastChar; i++) {
			char c = password.charAt(i);
			
			// Falls das Zeichen eine Ziffer ist, wird der Counter specialBetweenChar um eins erhoeht.
			if (Character.isDigit(c)) {
				specialBetweenChar++;
			} 
			
			// Falls das Zeichen kein Klein- oder Grossbuchstabe ist, wird der Counter specialBetweenChar um eins erhoeht.
			else if(!Character.isLowerCase(c) && !Character.isUpperCase(c)) {
				specialBetweenChar++;
			}	
		}
		
		try {
			String resWordlist = this.compareWordlist(password);
			
			if (!resWordlist.isEmpty()) {
				returnMessage.add(this.compareWordlist(password));
			}
		} catch (IOException e) {
			throw new RuntimeException(e);
		} 

		// Falls die Laenge des Passwortes kleiner als acht ist, wird die Bedingung isCorrect auf "false" gesetzt.
		if(password.length() < this.minLength) {
			returnMessage.add(Messages.getString("Message.0", this.minLength));
		}
		
		
		// Falls die Anzahl der Buchstaben kleiner als zwei ist, wird die Bedingung isCorrect auf "false" gesetzt.
		if((cntUpperCase + cntLowerCase) < this.numCase) {
			returnMessage.add(Messages.getString("Message.1", this.numCase));
		}
	
		//Falls die Anzahl der Grossbuchstaben kleiner als eins ist, wird die Bedingung isCorrect auf "false" gesetzt.
		if(cntUpperCase < this.numUpperCase) {
			returnMessage.add(Messages.getString("Message.2", this.numUpperCase));
		}
		
		//Falls die Anzahl der Kleinbuchstaben kleiner als eins ist, wird die Bedingung isCorrect auf "false" gesetzt.	
		if(cntLowerCase < this.numLowerCase) {
			returnMessage.add(Messages.getString("Message.3", this.numLowerCase));
		}

		//Falls die Anzahl der Sonderzeichen kleiner als eins ist, wird die Bedingung isCorrect auf "false" gesetzt.
		if(cntSpecial < this.numSpecial) {
			returnMessage.add(Messages.getString("Message.4", this.numSpecial));
		}
		
		//Falls die Anzahl der Ziffern kleiner als zwei ist, wird die Bedingung isCorrect auf "false" gesetzt.
		if(cntNumber < this.numNumbers) {
			returnMessage.add(Messages.getString("Message.5", this.numNumbers));
		}
	
		//Falls die Anzahl der Ziffern bzw. Sonderzeichen innerhalb des Passwortes kleiner als eins ist, wird die Bedingung isCorrect auf "false" gesetzt. 
		if(specialBetweenChar < this.numSpecialBetweenChar) {
			returnMessage.add(Messages.getString("Message.6", this.numSpecialBetweenChar));
		}
		
		//@return boolean Kriterien wurden erfuellt bzw. nicht erfuellt.
		return returnMessage;
	}
	
	/**
	 * 
	 * @param password
	 * @return
	 */
	public static int calculateEntropy(final String password) {
		final Map<Character, Integer> cntChars = new HashMap<Character, Integer>();
		int base = 0;
		double length = 0.0;
		boolean numeric = false;
		boolean upperCase = false;
		boolean lowerCase = false;
		boolean punct = false;

		for (char c : password.toCharArray()) {

			if (cntChars.containsKey(c)) {
				int num = cntChars.get(c);
				if (num < 4) {
					length += (1-num*0.2);
				}
				else {
					length += 0.2;
				}
				cntChars.put(c, ++num);
			} else {
				cntChars.put(c, 1);
				length += 1;
			}
			
			if (Pattern.matches("\\d", String.valueOf(c))) {
				numeric = true;
			}
			else if(Pattern.matches("[\\p{Upper}ÄÜÖ]", String.valueOf(c))) {
				upperCase = true;
			}
			else if(Pattern.matches("[\\p{Lower}äöü]", String.valueOf(c))) {
				lowerCase = true;
			}
			else if(Pattern.matches("\\p{Punct}", String.valueOf(c))) {
				punct = true;
			}
		}
		
		if (numeric) {
			base += 10;
		}	
		if(lowerCase) {
			base += 29;
		}
		if(upperCase) {
			base += 29;
		}
		if(punct) {
			base += 32;
		}
		
		final int entropy = new Double(Math.log(Math.pow(base, length)) / Math.log(2)).intValue();
		return entropy;
	}
	
	public String getName() {
		return name;
	}
	
	public String compareWordlist(String password) throws IOException {
		String line;
		String returnMessage = "";
		
		InputStream is =  PasswordChecker.class.getResourceAsStream("NAMES.DIC");
		InputStreamReader isr;
		try {
			isr = new InputStreamReader(is, "UTF8");
			BufferedReader br = new BufferedReader(isr);
			
			while ((line = br.readLine()) != null) {
				if (password.toLowerCase().contains(line)) {
					returnMessage = Messages.getString("Message.7");
					break;						
				}
			}
		}  catch (IOException e) {
			throw new IOException(Messages.getString("Exception.2"));
		}
		return returnMessage;
	}
}
